package nl.tudelft.lifetiles.sequence.controller;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.ResourceBundle;
import java.util.Set;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections;
import javafx.collections.ObservableMap;
import javafx.fxml.FXML;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.CheckBoxTableCell;
import nl.tudelft.lifetiles.core.controller.AbstractController;
import nl.tudelft.lifetiles.core.util.Logging;
import nl.tudelft.lifetiles.core.util.Message;
import nl.tudelft.lifetiles.notification.controller.NotificationController;
import nl.tudelft.lifetiles.notification.model.NotificationFactory;
import nl.tudelft.lifetiles.sequence.model.Sequence;
import nl.tudelft.lifetiles.sequence.model.SequenceEntry;
import nl.tudelft.lifetiles.sequence.model.SequenceMetaParser;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
/**
* The controller of the data view.
*
* @author Joren Hammudoglu
*
*/
public class SequenceController extends AbstractController {
/**
* Shout message indicating a sequence has been set as reference.
*/
public static final Message REFERENCE_SET = Message.create("referenceSet");
/**
* The default reference sequence name.
*/
private static final String DEFAULT_REFERENCE = "TKK_REF";
/**
* The sequence table.
*/
@FXML
private TableView<SequenceEntry> sequenceTable;
/**
* The table column indiciating sequence visibility.
*/
@FXML
private TableColumn<SequenceEntry, Boolean> visibleColumn;
/**
* The table column indiciating if a sequence is the reference.
*/
@FXML
private TableColumn<SequenceEntry, Boolean> referenceColumn;
/**
* The model of sequences.
*/
private Map<String, Sequence> sequences;
/**
* Set containing the currently visible sequences.
*/
private Set<Sequence> visibleSequences;
/**
* The sequence entries for in the table.
*/
private ObservableMap<String, SequenceEntry> sequenceEntries;
/**
* The index of the reference sequence entry.
*/
private String reference;
/**
* The listeners for the visible properties.
*/
private Map<SequenceEntry, ChangeListener<? super Boolean>> visibleListeners;
/**
* {@inheritDoc}
*/
@Override
public void initialize(final URL location, final ResourceBundle resources) {
registerShoutListeners();
initializeTable();
visibleListeners = new HashMap<>();
}
/**
* Register the shout listeners.
*/
// checkstyle bug causes false positives in our assert, so suppress.
@SuppressWarnings("checkstyle:genericwhitespace")
private void registerShoutListeners() {
listen(Message.LOADED, (sender, subject, args) -> {
if (!"sequences".equals(subject)) {
return;
}
// eclipse and PMD disagree on whether parentheses are
// needed
assert args[0] instanceof Map<?, ?>;
@SuppressWarnings("unchecked")
Map<String, Sequence> sequences = (Map<String, Sequence>) args[0];
load(sequences);
});
listen(Message.FILTERED, (sender, subject, args) -> {
assert args.length == 1;
assert args[0] instanceof Set<?>;
@SuppressWarnings("unchecked")
Set<Sequence> sequences = (Set<Sequence>) args[0];
updateVisible(sequences);
});
listen(Message.RESET, (sender, subject, args) -> {
Set<Sequence> visibleSet = new HashSet<>(sequences.values());
shout(Message.FILTERED, "", visibleSet);
});
listen(Message.OPENED,
(sender, subject, args) -> {
if (!"meta".equals(subject)) {
return;
}
assert args[0] instanceof File;
SequenceMetaParser parser = new SequenceMetaParser();
try {
parser.parse((File) args[0]);
addMetaData(parser.getColumns(), parser.getData());
} catch (IOException exception) {
Logging.exception(exception);
shout(NotificationController.NOTIFY, "",
new NotificationFactory()
.getNotification(exception));
}
});
}
/**
* Add the meta data to the appropriate sequence entries.
*
* @param columns
* The column names
* @param data
* The actual data
*/
private void addMetaData(final List<String> columns,
final Map<String, Map<String, String>> data) {
for (Entry<String, Map<String, String>> sequenceMeta : data.entrySet()) {
String identifier = sequenceMeta.getKey();
if (sequenceEntries.containsKey(identifier)) {
SequenceEntry sequenceEntry = sequenceEntries.get(identifier);
sequenceEntry.setMetaData(sequenceMeta.getValue());
}
}
addMetaColumns(columns);
}
/**
* Load in the new sequences.
*
* @param sequences
* the new sequences
*/
private void load(final Map<String, Sequence> sequences) {
this.sequences = sequences;
this.visibleSequences = new HashSet<>(sequences.values());
initializeEntries(sequences);
populateTable();
}
/**
* Initialize and populate the table.
* TODO display colors
*/
private void populateTable() {
sequenceTable.setItems(FXCollections
.observableArrayList(sequenceEntries.values()));
}
/**
* Initialize the table.
*/
private void initializeTable() {
sequenceTable.setEditable(true);
visibleColumn.setCellFactory(CheckBoxTableCell
.forTableColumn(visibleColumn));
visibleColumn.setEditable(true);
referenceColumn.setCellFactory(CheckBoxTableCell
.forTableColumn(referenceColumn));
referenceColumn.setEditable(true);
}
/**
* Add the meta data columns to the table.
*
* @param columns
* the names of the columns
*/
private void addMetaColumns(final List<String> columns) {
for (String columnName : columns) {
// purpose of this method is to create these.
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
TableColumn<SequenceEntry, String> column = new TableColumn<>(
columnName);
column.setCellValueFactory(entry -> entry.getValue().metaProperty(
columnName));
sequenceTable.getColumns().add(column);
}
}
/**
* Generate Sequence entries from sequences and store them.
*
* @param sequences
* the sequences
*/
// suppress a false positive on SequenceEntry
@SuppressFBWarnings("DLS_DEAD_LOCAL_STORE")
private void initializeEntries(final Map<String, Sequence> sequences) {
sequenceEntries = FXCollections.observableHashMap();
for (Sequence sequence : sequences.values()) {
SequenceEntry sequenceEntry = SequenceEntry.fromSequence(sequence);
String identifier = sequence.getIdentifier();
if (identifier.equals(DEFAULT_REFERENCE)) {
sequenceEntry = SequenceEntry
.fromSequence(sequence, true, true);
reference = identifier;
shout(SequenceController.REFERENCE_SET, "", sequence);
} else {
sequenceEntry = SequenceEntry.fromSequence(sequence);
}
addVisibilityListener(sequenceEntry);
addReferenceListener(sequenceEntry);
sequenceEntries.put(identifier, sequenceEntry);
}
}
/**
* Add listener to the visible and reference properties of a sequence entry.
*
* @param entry
* the sequence entry
*/
private void addVisibilityListener(final SequenceEntry entry) {
final ChangeListener<? super Boolean> listener;
if (visibleListeners.containsKey(entry)) {
listener = visibleListeners.get(entry);
} else {
listener = (value, previous, current) -> {
if (previous != current) {
updateVisible(entry, current);
shout(Message.FILTERED, "", visibleSequences);
}
};
visibleListeners.put(entry, listener);
}
entry.visibleProperty().addListener(listener);
}
/**
* Remove the visibility listener from the entry.
*
* @param entry
* the entry
*/
private void removeVisibilityListener(final SequenceEntry entry) {
if (!visibleListeners.containsKey(entry)) {
throw new IllegalArgumentException("Entry " + entry.getIdentifier()
+ " has no listener");
}
entry.visibleProperty().removeListener(visibleListeners.get(entry));
}
/**
* Add listener to the sequence entry's reference property.
*
* @param entry
* the sequence entry.
*/
private void addReferenceListener(final SequenceEntry entry) {
entry.referenceProperty().addListener(
(value, previous, current) -> {
if (previous != current && !previous) {
SequenceEntry previousRef = sequenceEntries
.get(reference);
previousRef.setReference(false);
String identifier = entry.getIdentifier();
reference = identifier;
shout(SequenceController.REFERENCE_SET, "",
sequences.get(identifier));
}
});
}
/**
* Update a sequence's visiblity and shout new visible sequences.
*
* @param entry
* the sequence entry
* @param visible
* whether the sequence became visible
*/
private void updateVisible(final SequenceEntry entry, final boolean visible) {
Sequence sequence = this.sequences.get(entry.getIdentifier());
if (visible) {
visibleSequences.add(sequence);
} else {
visibleSequences.remove(sequence);
}
}
/**
* Update to the new visibles.
*
* @param visibles
* the new visibles
*/
private void updateVisible(final Set<Sequence> visibles) {
if (!sequences.values().containsAll(visibles)) {
throw new IllegalArgumentException("Unknown sequences");
}
for (Sequence sequence : sequences.values()) {
boolean visible = visibles.contains(sequence);
SequenceEntry entry = sequenceEntries.get(sequence.getIdentifier());
// temporarily stop listening to prevent circular shouting
removeVisibilityListener(entry);
entry.visibleProperty().setValue(visible);
addVisibilityListener(entry);
}
visibleSequences = visibles;
}
}